7.4 Fortgeschrittene Delegat-Techniken  
7.4.1 Eine Beispielanwendung  
Stellen Sie sich vor, Sie hätten den Auftrag bekommen, eine Software zu entwickeln, die eine Pumpenanlage zum Befüllen des Schwimmbeckens eines Schwimmbades ansteuert. Es handelt sich bei dieser Anlage um Pumpen verschiedener Hersteller. Grundsätzlich sollen alle Pumpen eingeschaltet werden, wenn das Becken gefüllt wird. Anzahl und Typ der Pumpen können dabei durchaus variieren. Ihre Software soll so flexibel sein, sich an solche Änderungen automatisch anpassen zu können. Wie kann das Problem am besten gelöst werden?
Man kann davon ausgehen, dass jede Pumpe anders angesteuert werden muss. Daher bietet es sich an, für jede in Frage kommende Pumpe eine eigene Klasse mit einer Methode zu entwickeln, aus der heraus die Pumpe gestartet wird. Wir wollen zunächst zwei Klassen bereitstellen, PumpeA und PumpeB, deren Methoden SwitchOnA und SwitchOnB für die komplexen Einschaltvorgänge stehen sollen.
| Class PumpeA
|
| Public Sub SwitchOnA()
|
| Console.WriteLine("Pumpe A wird eingeschaltet")
|
| End Sub
|
| End Class
|
| Class PumpeB
|
| Public Sub SwitchOnB()
|
| Console.WriteLine("Pumpe B wird eingeschaltet")
|
| End Sub
|
| End Class
|
Eine Pumpenanlage, in der je eine Pumpe von jedem Typ installiert ist, soll nun eingeschaltet werden. Dazu implementieren wir eine Klasse, welche die Ansteuerung des Startvorgangs der Pumpen übernimmt. Eine weitere Klasse ist als Komponente in der Benutzeranwendung implementiert und ruft eine Methode in der pumpensteuernden Klasse auf, mit welcher der Startvorgang in Gang gesetzt wird.
Wenden wir uns zunächst der Klasse zu, die eine Methode bereitstellen soll, aus der heraus die pumpenspezifischen Startmethoden aufgerufen werden. Im einfachsten Fall könnte der Code wie folgt lauten:
| Class ControlPumps
|
| Public Sub StartAllPumps()
|
| Dim p1 As New PumpeA
|
| Dim p2 As New PumpeB
|
| p1.SwitchOnA()
|
| p2.SwitchOnB()
|
| End Sub
|
| End Class
|
 Hier klicken, um das Bild zu Vergrößern
Abbildung 7.3 Starten der Pumpen über eine steuernde Klasse
Ein Client könnte nun mit
| Dim obj As New ControlPumps
|
| obj.StartAllPumps()
|
zwar das Füllen des Beckens in die Wege leiten, aber dieser Ansatz hat einen ganz wesentlichen Nachteil: Ihm fehlt die Flexibilität, der Steuerung eine oder auch mehrere Pumpen dynamisch hinzufügen zu können und deren spezifische Startmethode aufzurufen. Die Klasse ControlPumps, die den Kern der gesamten Anwendung darstellt, müsste mit jeder neu installierten Pumpe ausgetauscht werden. Das gilt selbstverständlich auch, wenn eine Pumpe deinstalliert wird. Was ist außerdem, wenn ein Pumpenhersteller einen neuen Typ auf den Markt bringt, der für den Schwimmbadbetreiber vielleicht aufgrund der Leistungsdaten interessant ist? Die Ergänzung oder der Austausch der installierten Pumpen wäre nicht ohne Komplikationen – und das nur, weil unsere Steuerungssoftware kläglich versagt.
Wir haben es mit zwei Problemen zu tun, die gelöst werden müssen:
| 1. |
das Starten einer beliebigen Anzahl von Pumpen aus der Methode StartAllPumps heraus |
| |
|
| 2. |
die Standardisierung des Aufrufs der spezifischen Startmethoden |
| |
|
Der zweite Punkt bringt uns sofort in Erinnerung, dass ein Delegat ein Funktionsprototyp ist und einen allgemeinen Methodenaufruf ermöglicht. Auch ohne bisher einen programmiertechnischen Ansatz entwickelt zu haben, wissen wir zumindest schon, welcher Weg uns zum Ziel führt.
Es fehlt im Wesentlichen nur noch eine Idee zur Lösung des ersten Punktes. Ein möglicher Lösungsansatz könnte ein Array sein, dessen Elemente die verschiedenen Pumpen referenzieren. Dieses Array könnte elementweise durchlaufen werden. Dazu bietet sich eine Collection an, beispielsweise ArrayList, die sich insbesondere durch einfache Programmierbarkeit auszeichnet.
Sehen wir uns den Code in der Klasse ControlPumps an, der unter Einbeziehung eines ArrayList-Objekts unsere Bedingungen erfüllt:
| Public Delegate Sub PumpDelegate()
|
| Class ControlPumps
|
| Private colPumps As New ArrayList
|
| Public Sub AddPump(ByVal newPump As PumpDelegate)
|
| colPumps.Add(newPump)
|
| End Sub
|
| Public Sub StartAllPumps()
|
| Dim del As PumpDelegate
|
| For Each del In colPumps
|
| del()
|
| Next
|
| End Sub
|
| End Class
|
Verantwortlich für das Verhalten der Klasse ist das Feld colPumps, das vom Typ der Collection ArrayList ist. Das Feld wird initialisiert, sobald ein Benutzer ControlPumps instanziiert. ColPumps dient aber nicht dazu, Pumpenobjekte zu verwalten. Diese spielen bei genauer Betrachtung eine eher untergeordnete Rolle, denn vielmehr sind die Methoden von Interesse, mit denen die Pumpen gestartet werden. Aus diesem Blickwinkel heraus drängt sich die Idee auf, vom Auflistungsobjekt Delegaten verwalten zu lassen, die einen entsprechenden Zeiger auf die zu einer Pumpe gehörende Startmethode kapseln.
Die Methode, mit der eine Pumpe – oder präzise ausgedrückt deren Startmethode – zu dem Objekt colPumps hinzugefügt wird, lautet AddPump. AddPump empfängt vom Aufrufer im Parameter newPump einen Delegaten vom Typ PumpDelegate. Dessen Referenz wird unter Aufruf der Methode Add dem ArrayList-Objekt übergebeben. Die Methode StartAllPumps ist an Einfachheit kaum noch zu übertreffen. In einer For Each-Schleife wird jedes Element der Auflistung colPumps durchlaufen und ausgeführt, was in unserem Beispiel zur Ausführung einer spezifischen Pumpenstartmethode führt.
Widmen wir uns nun dem Client und betrachten den Code, mit dem ein Delegat, der bekanntermaßen die Startmethode eines bestimmten Pumpenobjekts beschreibt, an die AddPump-Methode übergeben wird:
| Dim obj As New ControlPumps
|
| Dim p1 As New PumpeA
|
| Dim pumpDel As PumpDelegate = _
|
| New PumpDelegate(AddressOf p1.SwitchOnA)
|
| obj.AddPump(pumpDel)
|
Wir besorgen uns zuerst sowohl eine Instanz der Klasse ControlPumps als auch die einer Pumpe. Im Codefragment handelt es sich um eine Pumpe vom Typ PumpeA. Um die Pumpe zu starten, benötigt das ControlPumps-Objekt einen Delegaten auf die Startmethode der Pumpe. Dieser wird in der dritten Codezeile erzeugt und in der vierten der AddPump-Methode als Argument übergeben.
Weil im weiteren Verlauf die Referenz auf den Delegaten nicht mehr benötigt wird, können wir den Programmcode auch etwas kürzer schreiben, indem wir den Delegaten direkt beim Aufruf der AddPump-Methode erzeugen:
| Dim p1 As New PumpeA
|
| obj.AddPump(New PumpDelegate(AddressOf p1.SwitchOnA))
|
Nehmen wir an, dass vier Pumpen gestartet werden sollen, würde die Definition der Benutzerklasse wie folgt lauten:
| ' ----------------------------------------------------------
|
| ' Beispiel: ...\Kapitel 7\SimpleDelegate
|
| ' ----------------------------------------------------------
|
| Module Module1
|
| Sub Main()
|
| Dim obj As New ControlPumps
|
| ' erste Pumpe
|
| Dim p1 As New PumpeA
|
| obj.AddPump(New PumpDelegate(AddressOf p1.SwitchOnA))
|
| ' zweite Pumpe
|
| Dim p2 As New PumpeB
|
| obj.AddPump(New PumpDelegate(AddressOf p2.SwitchOnB))
|
| ' dritte Pumpe
|
| Dim p3 As New PumpeA
|
| obj.AddPump(New PumpDelegate(AddressOf p3.SwitchOnA))
|
| ' vierte Pumpe
|
| Dim p4 As New PumpeA
|
| obj.AddPump(New PumpDelegate(AddressOf p4.SwitchOnA))
|
| ' Pumpen starten
|
| obj.StartAllPumps()
|
| Console.ReadLine()
|
| End Sub
|
| End Module
|
Die Ausgabe im Befehlsfenster lautet:
| Pumpe A wird eingeschaltet
|
| Pumpe B wird eingeschaltet
|
| Pumpe A wird eingeschaltet
|
| Pumpe A wird eingeschaltet
|
Das Ergebnis ist perfekt, wir haben das Ziel erreicht. Die Klasse ControlPumps ist so flexibel implementiert, dass sie nicht nur die Belange eines Schwimmbads abdeckt, sondern überall dort eingesetzt werden könnte, wo Pumpen der Reihe nach eingeschaltet werden müssen. Eigentlich ist diese Aussage falsch, denn wir können sie sogar auf jedwede beliebige Komponente ausdehnen, unter der Voraussetzung, dass in der Komponente eine parameterlose Methode aufgerufen werden soll. Die Verhaltensweise, die von der Methode beschrieben wird, spielt dabei keine Rolle – alles dank der Delegaten.
7.4.2 Multicast-Delegaten  
.NET bietet die Möglichkeit, mehrere Delegaten zu einem einzigen zusammenzufassen. Dadurch entsteht ein Delegaten-Verbund, der auch als Multicast-Delegate bezeichnet wird. Der Vorteil ist, dass durch den Aufruf eines Delegaten mehrere Delegaten der Reihe ausgeführt werden können. Sehen Sie sich dazu noch einmal unser Beispiel SimpleDelegate des vorhergehenden Abschnitts an. Es wird darin von vier Pumpen ausgegangen, deren Startmethoden nacheinander von je einem Delegaten eingebunden werden. In der steuernden Klasse bedarf es eines Objekt-Arrays, um alle Delegaten zu verwalten.
Der Programmcode ist wesentlich einfacher und übersichtlicher, wenn ein Multicast-Delegate die Aufgabe übernimmt. Dies soll das folgende Beispiel zeigen, das unter denselben Vorgaben wie das Beispiel SimpleDelegate entwickelt worden ist. Die Klassendefinitionen der Pumpen haben sich natürlich auch jetzt nicht verändert.
| ' ----------------------------------------------------------
|
| ' Beispiel: ...\Kapitel 7\MulticastDelegate
|
| ' ----------------------------------------------------------
|
| Public Delegate Sub PumpDelegate()
|
| Module Module1
|
| Sub Main()
|
| Dim obj As New ControlPumps
|
| ' Array vom Typ PumpDelegate
|
| Dim del(3) As PumpDelegate
|
| ' Pumpenobjekte erzeugen
|
| Dim p1 As New PumpeA
|
| Dim p2 As New PumpeB
|
| Dim p3 As New PumpeA
|
| Dim p4 As New PumpeA
|
| ' die Startmethoden der Pumpen durch ein Delegate-
|
| ' Objekt beschreiben
|
| del(0) = New PumpDelegate(AddressOf p1.SwitchOnA)
|
| del(1) = New PumpDelegate(AddressOf p2.SwitchOnB)
|
| del(2) = New PumpDelegate(AddressOf p1.SwitchOnA)
|
| del(3) = New PumpDelegate(AddressOf p1.SwitchOnA)
|
| ' alle Delegaten kombinieren
|
| Dim arrDel As PumpDelegate = PumpDelegate.Combine(del)
|
| ' das Delegate-Array an die Steuerklasse übergeben
|
| obj.AddPump(arrDel)
|
| ' die Pumpen starten
|
| obj.StartAllPumps()
|
| Console.ReadLine()
|
| End Sub
|
| End Module
|
| ' Steuerklasse
|
| Class ControlPumps
|
| Private delPumps As PumpDelegate
|
| Public Sub AddPump(ByVal pumps As PumpDelegate)
|
| delPumps = pumps
|
| End Sub
|
| Public Sub StartAllPumps()
|
| delPumps()
|
| End Sub
|
| End Class
|
| ...
|
Werfen wir zuerst einen Blick auf den Code in Main. Es fällt als Erstes auf, dass die den Pumpenobjekten zugeordneten Delegaten nun zu Elementen des Arrays del werden. Das hätten wir natürlich auch schon im Code des Beispiels SimpleDelegate so machen können. Nun steckt aber eine ganz bestimmte Absicht dahinter, die in der darauf folgenden Anweisung deutlich wird:
| Dim arrDel As PumpDelegate = PumpDelegate.Combine(del)
|
Da Delegaten wie alles in der .NET-Welt Objekte sind, wird dieser Typ durch eine eigene Klasse im Namespace System beschrieben: Delegate. Mit der statischen Methode Combine dieses Typs lassen sich mehrere Delegaten miteinander verknüpfen. Combine ist wie folgt überladen:
| Public Shared Function Combine(ParamArray del As Delegate()) _
|
| As Delegate
|
| Public Shared Function Delegate Combine(Delegate, Delegate) _
|
| As Delegate
|
Sie können als Argument entweder ein Array vom Typ Delegate übergeben oder haben die Alternative, zwei Delegaten miteinander zu verknüpfen. Der Rückgabewert ist in beiden Fällen ein Delegat, oder präziser, es handelt sich um ein Multicast-Delegate, für den es in der Klassenbibliothek eine eigene Typdefinition gibt, die von Delegate abgeleitet ist: MulticastDelegate. Der Rückgabewert wird nach einer expliziten Konvertierung dem benutzerdefinierten Delegaten zugewiesen.
Wir haben nun eine Objektvariable namens arrDel, die ein Multicast-Delegate referenziert, der seinerseits vier Singlecast-Delegaten kombiniert. Dem Objekt der Steuerklasse müssen wir jetzt nur noch die Referenz arrDel übergeben und können uns daher in der Klassendefinition von ControlPumps das aggregierte ArrayList-Objekt ersparen. Das hat auch zur Folge, dass die Methoden AddPump und StartAllPumps an die neue Situation angepasst werden müssen. Insgesamt reduziert sich der Code und wird dadurch deutlich einfacher.
Führen Sie das Programm aus, wird an der Konsole dieselbe Ausgabe erscheinen wie im Beispiel aus Abschnitt 7.4.1:
| Pumpe A wird eingeschaltet
|
| Pumpe B wird eingeschaltet
|
| Pumpe A wird eingeschaltet
|
| Pumpe A wird eingeschaltet
|
Methoden eines Multicast-Delegaten
Jeder Delegat steht für eine Liste von Methodenaufrufen, die durchlaufen wird, sobald der Delegat ausgeführt wird. Im Falle eines Singlecast-Delegaten enthält diese Liste nur ein Element, bei einem Multicast-Delegaten können es mehrere sein. Auf diese Aufrufliste können Sie mit der Methode GetInvocationList der Klasse Delegate bzw. MulticastDelegate zugreifen, der Rückgabewert ist ein Delegate-Array.
In der Klasse Delegate ist diese Methode wie folgt definiert:
| Public Overridable Function GetInvocationList As Delegate()
|
Um eine Methode zu der Aufrufliste hinzuzufügen oder von ihr zu entfernen, definiert die Delegate-Klasse die beiden statischen Methoden Combine und Remove. Wir hatten in unserem Beispiel oben einen Multicast-Delegaten erzeugt, indem wir vier Singlecast-Delegaten in ein Array zusammengefasst und als Argument der Combine-Methode übergeben haben. Die überladene Version dieser Methode wollen wir uns zusammen mit der Remove-Methode anschauen:
| Public Shared Function Delegate Combine(Delegate, Delegate) _
|
| As Delegate
|
| Public Shared Function Remove(source As Delegate, _
|
| value As Delegate) As Delegate
|
Beide Parameterlisten sind identisch und erwarten sowohl im ersten als auch im zweiten Argument die Referenz auf einen Delegaten. Dem ersten Parameter wird die Referenz auf den Delegaten übergeben, zu dessen Aufrufliste ein weiterer Delegat hinzugefügt bzw. im Fall der Remove-Methode entfernt werden soll. Der zweite Parameter beschreibt den hinzuzufügenden bzw. zu entfernenden Delegaten. Dazu ein kleines Beispiel:
| Dim del1 As PumpDelegate = New PumpDelegate(AddressOf p1.SwitchOnA)
|
| Dim del2 As PumpDelegate = New PumpDelegate(AddressOf p2.SwitchOnB)
|
| del2 = Delegate.Combine(del2, del1)
|
| ...
|
| del = Delegate.Remove(del2, del1)
|
In der dritten Codezeile wird der Delegat zu einem Multicast-Delegaten, in der letzten wird diese Zuordnung wieder aufgehoben. Es ist möglich, im zweiten Argument einen Multicast-Delegaten anzugeben, letztendlich verkleinert sich dadurch allerdings nicht der Programmcode. Bei einer Kombination mehrerer Delegaten ist daher die Variante mit der Übergabe eines Arrays vorzuziehen.
Interessiert der Name der von einem Delegaten gekapselten Methode, lässt sich das durch die schreibgeschützte Eigenschaft Method nebst weiteren Informationen in Erfahrung bringen. Der Aufruf dieser Eigenschaft liefert als Rückgabewert die Referenz auf ein Objekt vom Typ System.Reflection.MethodInfo, das die unterschiedlichsten Informationen zu einer Methode bereitstellt, beispielsweise über die Instanzeigenschaft Name den Namen der von einem Delegaten eingeschlossenen Methode.
| Console.WriteLine(del1.Method().Name)
|
Die ebenfalls schreibgeschützte Eigenschaft Target liefert eine Referenz auf das Objekt, dessen Instanzmethode der aktuelle Delegat aufruft:
| Public ReadOnly Property Target As Object
|
|